﻿using JESAI.DistributedLock.Internal;
using JESAI.DistributedLock.Internal.Data;
using System;
using System.Data;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace JESAI.DistributedLock.MySql;

/// <summary>
/// 基于GET_lock系列函数为MySQL或MariaDB实现分布式锁
/// </summary>
public sealed partial class MySqlDistributedLock : IInternalDistributedLock<MySqlDistributedLockHandle>
{
    /// <summary>
    /// 来自 https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html
    /// </summary>
    internal const int MaxNameLength = 64;

    private readonly IDbDistributedLock _internalLock;

    /// <summary>
    /// 使用给定的<paramref name="name"/>构造一个锁，该锁使用提供的<paramref-name=“connectionString”/>和<paramref name="options"/>进行连接。
    /// 
    /// 除非指定了<paramref name="exactName"/>，否则将转义/散列<paramref name="name"/>以确保名称有效性。
    /// </summary>
    public MySqlDistributedLock(string name, string connectionString, Action<MySqlConnectionOptionsBuilder>? options = null, bool exactName = false)
        : this(name, exactName, n => CreateInternalLock(n, connectionString, options))
    {
    }

    /// <summary>
    /// 构造一个具有给定<paramref name="name"/>的锁，该锁使用提供的<paramref name="connection" />进行连接。
    /// 
    /// 除非指定了<paramref name="exactName"/>，否则将转义/散列<paramref name="name"/>以确保名称有效性。
    /// </summary>
    public MySqlDistributedLock(string name, IDbConnection connection, bool exactName = false)
        : this(name, exactName, n => CreateInternalLock(n, connection))
    {
    }

    /// <summary>
    /// 构造一个具有给定<paramref name="name"/>的锁，该锁使用提供的<paramref name="transaction" />中的连接进行连接。
    /// 
    /// 请注意，锁的作用域不限于<paramref name="transaction"/>，并且必须在事务结束前明确释放。
    /// 但是，此构造函数允许锁参与连接上正在进行的事务。
    /// 
    /// 除非指定了<paramref name="exactName"/>，否则将转义/散列<paramref name="name"/>以确保名称有效性。
    /// </summary>
    public MySqlDistributedLock(string name, IDbTransaction transaction, bool exactName = false)
        : this(name, exactName, n => CreateInternalLock(n, transaction))
    {
    }

    private MySqlDistributedLock(string name, bool exactName, Func<string, IDbDistributedLock> internalLockFactory)
    {
        if (name == null) { throw new ArgumentNullException(nameof(name)); }

        if (exactName)
        {
            if (name.Length > MaxNameLength) { throw new FormatException($"{nameof(name)}: must be at most {MaxNameLength} characters"); }
            if (name.Length == 0) { throw new FormatException($"{nameof(name)}: must not be empty"); }
            if (name.ToLowerInvariant() != name) { throw new FormatException($"{nameof(name)}: must not container uppercase letters"); }
            this.Name = name;
        }
        else
        {
            this.Name = GetSafeName(name);
        }

        this._internalLock = internalLockFactory(this.Name);
    }

    /// <summary>
    /// 实现 <see cref="IDistributedLock.Name"/>
    /// </summary>
    public string Name { get; }

    ValueTask<MySqlDistributedLockHandle?> IInternalDistributedLock<MySqlDistributedLockHandle>.InternalTryAcquireAsync(TimeoutValue timeout, CancellationToken cancellationToken) =>
        this._internalLock.TryAcquireAsync(timeout, new MySqlUserLock(), cancellationToken, contextHandle: null).Wrap(h => new MySqlDistributedLockHandle(h));

    private static string GetSafeName(string name) =>
        ToSafeName(
            name,
            MaxNameLength,
            convertToValidName: s =>
            {
                if (s.Length == 0) { return "__empty__"; }
                return s.ToLowerInvariant();
            },
            hash: ComputeHash
        );

    private static string ToSafeName(string name, int maxNameLength, Func<string, string> convertToValidName, Func<byte[], string> hash)
    {
        if (name == null) { throw new ArgumentNullException(nameof(name)); }

        var validBaseLockName = convertToValidName(name);
        if (validBaseLockName == name && validBaseLockName.Length <= maxNameLength)
        {
            return name;
        }

        var nameHash = hash(Encoding.UTF8.GetBytes(name));

        if (nameHash.Length >= maxNameLength)
        {
            return nameHash.Substring(0, length: maxNameLength);
        }

        var prefix = validBaseLockName.Substring(0, Math.Min(validBaseLockName.Length, maxNameLength - nameHash.Length));
        return prefix + nameHash;
    }

    private static string ComputeHash(byte[] bytes)
    {
        using var sha = SHA512.Create();
        var hashBytes = sha.ComputeHash(bytes);

        // 我们截断为160位，即Base32的32个字符。这仍然可以为我们提供良好的抗冲突性，但允许64个字符的名称包含原始提供名称的大部分，这有利于调试。请参阅
        // https://crypto.stackexchange.com/questions/9435/is-truncating-a-sha512-hash-to-the-first-160-bits-as-secure-as-using-sha1#:~:text=Yes.,time%20is%20still%20pretty%20big
        const int Base32CharBits = 5;
        const int HashLengthInChars = 160 / Base32CharBits;

        //我们使用Base32是因为它不区分大小写（就像MySQL一样），并且比Base16 RFC 4648更紧凑 https://en.wikipedia.org/wiki/Base32
        const string Base32Alphabet = "abcdefghijklmnopqrstuvwxyz234567";

        var chars = new char[HashLengthInChars];
        var byteIndex = 0;
        var bitBuffer = 0;
        var bitsRemaining = 0;
        for (var charIndex = 0; charIndex < chars.Length; ++charIndex)
        {
            if (bitsRemaining < Base32CharBits)
            {
                bitBuffer |= hashBytes[byteIndex++] << bitsRemaining;
                bitsRemaining += 8;
            }
            chars[charIndex] = Base32Alphabet[bitBuffer & 31];
            bitBuffer >>= Base32CharBits;
            bitsRemaining -= Base32CharBits;
        }

        return new string(chars);
    }

    private static IDbDistributedLock CreateInternalLock(string name, string connectionString, Action<MySqlConnectionOptionsBuilder>? options)
    {
        if (connectionString == null) { throw new ArgumentNullException(nameof(connectionString)); }

        var (keepaliveCadence, useMultiplexing) = MySqlConnectionOptionsBuilder.GetOptions(options);

        if (useMultiplexing)
        {
            return new OptimisticConnectionMultiplexingDbDistributedLock(name, connectionString, MySqlMultiplexedConnectionLockPool.Instance, keepaliveCadence);
        }

        return new DedicatedConnectionOrTransactionDbDistributedLock(name, () => new MySqlDatabaseConnection(connectionString), useTransaction: false, keepaliveCadence);
    }

    private static IDbDistributedLock CreateInternalLock(string name, IDbConnection connection)
    {
        if (connection == null) { throw new ArgumentNullException(nameof(connection)); }

        return new DedicatedConnectionOrTransactionDbDistributedLock(name, () => new MySqlDatabaseConnection(connection));
    }

    private static IDbDistributedLock CreateInternalLock(string name, IDbTransaction transaction)
    {
        if (transaction == null) { throw new ArgumentNullException(nameof(transaction)); }

        // 注意：我们在这里传递useTransaction:false，因为MYSQL锁始终是会话范围的；我们只支持对事务进行锁定，以便您的锁可以参与连接。
        return new DedicatedConnectionOrTransactionDbDistributedLock(name, () => new MySqlDatabaseConnection(transaction), useTransaction: false, keepaliveCadence: Timeout.InfiniteTimeSpan);
    }
}
